| 함수 | 패키지 | 역할 | 주요 논항 및 예시 |
|---|---|---|---|
| array() | Base R | 딥러닝 입력용 다차원 배열(텐서)을 초기화함(원핫 인코딩용) | array(0L, dim = c(samples, timesteps, features)) |
| layer_input() | keras3 | 모델의 ’입구’를 정의함(데이터의 형태 shape 지정) | layer_input(shape = c(NA, num_encoder_tokens)) |
| layer_lstm() | keras3 | LSTM 레이어를 정의함(기억용량 설정, 마지막 타입스텝의 출력+상태 둘 다 반환 여부 설정, 마지막 타입스텝의 출력+모든 타임스텝의 은닉상태 반환 여부 설정) | layer_lstm(units = 256, return_state = TRUE, return_sequences = TRUE) |
| layer_dense() | keras3 | 뉴런과 활성화 함수(softmax 등)를 연결하는 출력층을 정의함 | layer_dense(units = vocab_size, activation = “softmax”) |
| keras_model() | keras3 | 입력과 출력을 연결하여 전체 모델을 조립함 | keras_model(inputs = c(enc_in, dec_in), outputs = dec_out) |
| compile() | keras3 | 모델의 학습 방식(옵티마이저, 손실함수)을 설정함 | compile(optimizer = “rmsprop”, loss = “categorical_crossentropy”) |
| fit() | keras3 | 준비된 데이터로 모델을 실제로 학습시킴 | fit(x = list(input1, input2), y = target, epochs = 20) |
| predict() | keras3 | 학습된 모델을 사용해 결과를 예측함 | predict(input_data, verbose = 0) |
| str_split() | tidyverse(stringr) | 문자열을 글자 단위 등으로 쪼갬 | str_split(text, ““) |
[그림 1] RNN의 원리와 작동방식
[그림 2] LSTM의 원리와 작동방식
[그림 3] Seq2Seq의 원리와 작동방식
ChatbotData.csv(Q & A 쌍)encoder_input_data(인코더 입력): 질문 원본. \(\Rightarrow\) [“뭐”, “해”]decoder_input_data(디코더 입력): 시작신호(\(t\))가 붙은 답변. \(\Rightarrow\) [“\(t\)”, “그”, “냥”, ” “,”있”, “어”]decoder_target_data(디코더 타겟): 종료신호(\(n\))가 붙고 한 칸씩 밀린
답변. \(\Rightarrow\) [“그”, “냥”, ”
“,”있”, “어”, “\(n\)”]encoder_input_data(“뭐해”)를 읽고
[문맥 벡터]를 만듦.decoder_input_data(“\(t\)그냥 있어”)를 한꺼번에
받음.decoder_target_data(“그냥 있어\(n\)”)와 정확히 일치하도록 훈련시킴.decoder_input_data)을 다음 스텝의
입력으로 넣어주어 학습속도를 높임. \(\Rightarrow\) 즉 \(t=1\)일 때 \(t\)를 넣고, \(t=2\)일 때 모델의 예측인 “밥” 대신 정답인
“그”를 강제로 주입함.encoder_model): “뭐 해?”를 읽고 이해한 다음
[문맥 벡터]를 생성함.decoder_model)
keras_model_sequential()이
아닌 Keras 함수형 API(functional API)를 사용함.keras_model_sequential()(순차 모델)
%>% 파이프라인처럼 레이어를 차곡차곡 쌓는 가장
간단한 방식임.keras_model(inputs = ..., outputs = ...)(함수형 API)
keras_model_sequential)
keras_model): 훨씬 자유로운 뷔페
접시 담기와 같음.
x를
함수 layer에 넣으면 출력 데이터 y가 나온다는
수학적 개념(\(y = f(x)\))을 그대로
코드로 구현한 것!# 1단계: 부품(layer) 정의(define)
# "이런 모양의 수도관(입력)과 필터(LSTM)를 쓸 거야"라고 선언만 함.
# (1) 입력층 정의(수도관 입구)
# shape: 들어올 데이터의 크기(시간 축은 가변적이니 NA)
encoder_inputs <- layer_input(shape = c(NA, num_encoder_tokens), name = "enc_input")
# (2) 처리층 정의(정수 필터)
# 아직 연결되지 않은 'LSTM 기기' 자체를 만듦.
encoder_lstm_layer <- layer_lstm(units = 256, return_state = TRUE, name = "enc_lstm")
# 2단계: 연결(connect) - 물(데이터)길 만들기
# "입력(x)을 LSTM 기계(f)에 통과시켜 결과(y)를 얻는다" => y = f(x)
# encoder_inputs(물)를 encoder_lstm_layer(필터)에 통과시킴.
# 그 결과물(outputs)과 상태(state)를 받음.
encoder_results <- encoder_lstm_layer(encoder_inputs)
# 필요한 것만 챙김(우리는 '기억'인 state만 필요)
encoder_states <- encoder_results[2:3]
# 3단계: 모델 생성(create model)
# "입수구는 여기고, 출수구는 저기인 기계야"라고 최종 확정.
# keras_model() 함수에 시작점(inputs)과 끝점(outputs)을 알려줌.
encoder_model <- keras_model(
inputs = encoder_inputs, # 시작: 질문 들어오는 곳
outputs = encoder_states # 끝: 문맥 벡터 나오는 곳
)
layer_name(input_tensor) 형태는 수학의 함수
\(f(x)\)와 같음. 데이터를 레이어에
통과시킨다는 직관적인 개념.keras_model(inputs, outputs)로 명확히 지정해줌.data = 0L: 배열을 0(정수형 L)으로 채움. 원핫 인코딩을
위해 0으로 가득 찬 배열을 먼저 만듦.dim = c(samples, timesteps, features): 3차원 배열
형태를 지정할 수 있음.
samples(샘플 수): 총 Q&A 쌍의 개수([EX]
2000개).timesteps(타임스텝)
max_encoder_seq_length \(\Rightarrow\) 100자.max_decoder_seq_length \(\Rightarrow\) 100자.features(특성 수)
features = 2,000이 됨. \(\Rightarrow\) 우리 실습의 데이터 벡터화
단계가 여기에 해당함(num_encoder_tokens,
num_decoder_tokens).output_dim = 256으로 설정했다면, “그”라는 글자는 256개의
실수([EX] [0.1, -0.5, 0.3, …])로 표현됨. 이때
features = 256이 됨.encoder_input_data[i, t, idx] <- 1은 “i번째 문장의,
t번째 글자”에 해당하는 idx번째 인덱스만 1로 바꾸는 원핫 인코딩
과정임.shape = c(NA, num_encoder_tokens): 이 입력을 통해
들어올 데이터의 ’형태’를 지정함(배치 크기[샘플 수]는 제외).
NA: 첫 번째 차원(타임스텝, 즉 문장 길이)은 가변적일 수
있음을 의미함(max_encoder_seq_length를 넣어도 되지만
NA가 더 유연함).num_encoder_tokens: 두 번째 차원(특성)은 질문사전의
크기, 즉 질문용 원핫 벡터의 크기임. \(\Rightarrow\) 학습 데이터에 있는 모든 질문
문장에 등장하는 ‘고유한 글자(음절’)의 총 개수!return_sequences()로 제어.return_state()로
제어.return_state = TRUE
FALSE(기본값): 마지막 타임스텝의 출력 외에 마지막
은닉상태와 셀 상태를 추가로 반환하지 않음.TRUE(중요!): 마지막 타임스텝의 [출력(output),
은닉상태(state_h), 셀 상태(state_c)]를 R 리스트 형태로 반환함.TRUE여야 함.layer_dense에 전달해야 하므로 반드시
TRUE여야 함.FALSE를 쓰면 안 됨!layer_dense(activation = "softmax")를 통과해야 비로소
나옴.return_sequences = FALSE
return_state = FALSE와의 차이점: 결과적으로만 보자면
마지막 시점의 출력만 반환하도록 한다는 점에서 차이는 없음. 그러나
return_state는 마지막 은닉상태와 셀 상태까지 추가로 반환할
것인지 여부를 결정하는 논항이므로 return_sequence와
관장하는 범위가 서로 다름.TRUE(과정 중심) vs. FALSE(결과 중심)
TRUE
FALSE(기본값)
[samples, timesteps, units] \(\Rightarrow\) [100, 10, 32]
samples(샘플 수): 한 번에 처리하는 문장(데이터)의 개수.
\(\Rightarrow\) 데이터가 100개라면
samples = 100.timesteps(타임스텝): 하나의 문장에 들어 있는 단어(또는
글자)의 개수. 시간순서대로 몇 번이나 입력을 넣어야 하는지를 의미함.
\(\Rightarrow\) 문장 최대 길이를
100글자로 정했다면 timesteps = 100(100번 동안 순차적으로
읽음).units
units 설정값과 같음. units가 작으면 단어의
의미를 대충 요약한 것이고, 크면 아주 상세하게 분석한 것임.units = 32라고 설정했다면, 이 레이어는 각 글자를
읽을 때마다 그 의미를 32개의 숫자로 번역해서 내놓음.return_sequences = TRUE를 사용하여 모든 시점의 출력을
시퀀스 형태로 반환받아야 온전한 답변문장을 얻을 수 있음!return_sequences = FALSE)
충분할 것 같음. 초기 Seq2Seq 모델은 실제로 그렇게 동작했음.return_sequences = TRUE로
설정해서 그 많은 중간과정의 기억들을 다 살려두는 걸까?FALSE의 한계: “기억의 병목현상”(닫힌 책 시험)
TRUE가 필요한 이유: “어텐션 메커니즘”(오픈 북 시험)
TRUE인가?
return_sequences = TRUE)! 디코더가 나중에 필요할
때마다 갖다 쓸 거니까!”return_sequences = FALSE 사용.
최종 요약본 하나에 의존. 긴 문장에 약함.return_sequences = TRUE 사용.
인코더가 가진 모든 순간의 기억을 디코더에게 넘겨줘서, 디코더가 매 순간
중요한 정보에 집중할 수 있게 함. 훨씬 강력함.return_sequences = TRUE를 설정하는 것이
일반적임.inputs: 모델의 입력층(들). 입력이 여러 개(인코더 입력 +
디코더 입력)일 경우 c(input1, input2)처럼
c(...)로 묶어 전달함.outputs: 모델의 최종 출력층.encoder_inputs): “질문” 데이터 전체.
encoder_input_data(원핫 인코딩된 3D 텐서).decoder_inputs): “시작 토큰(t)”이 포함된
“답변” 데이터(교사강요용). \(\Rightarrow\) 훈련을
시켜야 하니까 인코더와 디코더 입력이 모두 필요한 것!
decoder_input_data(원핫 인코딩된 3D 텐서).outputs = ...)
decoder_outputs): “종료 토큰(n)”이 포함된
“답변 정답” 데이터.decoder_target_data.x: 훈련 입력 데이터. keras_model 정의 시
inputs = c(encoder_inputs, decoder_inputs)로 두 개(인코더,
디코더)를 지정했으므로, x에도 list(encoder_input_data,
decoder_input_data)로 두 개를 순서에 맞게 전달해야 함.y: 훈련 타겟 데이터. \(\Rightarrow\) 여기서는
decoder_target_data.batch_size: 한 번의 가중치 업데이트에 사용할 샘플(문장
쌍) 뭉치의 개수([EX] 64개).epochs: 전체 데이터셋을 몇 번 반복 학습할지 결정.validation_split: 훈련 데이터 중 일부([EX] 20%)를
검증용으로 사용하여, 모델이 과적합(overfitting)되고 있지는 않은지
모니터링함.decoder_lstm이 encoder_states를
initial_state로 받아, 질문의 문맥을 갖고 답변 생성을
시작함.decoder_model이 루프를 돌 때, t=2
시점의 initial_state는 t=1 시점에서 반환된
new_state가 됨.while 루프(답변 생성이 끝날 때까지 계속
도는 반복문).decoder_model).states_value.initial_state 주입): 디코더 선수가 인코더가 준 첫
번째 바통을 손에 쥠. 이것이 \(t=1\)
시점의 initial_state가 됨.new_state 반환)new_state)!initial_state 주입): 이제 두 번째 글자를
예측해야 함. 다시 출발선에 선 디코더 선수는 어떤 바통을 쥐어야 할까?
\(\Rightarrow\) 바로 직전 바퀴(\(t=1\))를 완주하고 받은 “업데이트된 새
바통”을 쥐어야 함! 즉 \(t=1\)에서
반환된 new_state가 \(t=2\)의 출발 준비물인
initial_state가 되는 것.decoder_model은 예측을 수행할 때마다 두 가지를 내놓음:
예측한 글자 & 업데이트된 기억(상태).initial_state)으로 다시 넣어주는 것.library(keras3): “새로운 R 운전대”를 사용함. 이는 Keras
3.0+의 함수체계(임베딩, 전처리, 모델링 API)를 R에서 사용하겠다는
의미임.library(tensorflow): TensorFlow
엔진(백엔드)을 사용함. Keras 3는 API일 뿐, 실제 계산을 수행할 엔진이
필요함. 우리는 그 엔진으로 TensorFlow를 지정할
것임.library(keras3)와
library(tensorflow)를 함께 사용하는 것은
TensorFlow를 백엔드로 사용하는 Keras 3를 R에서 가장
올바른 방법으로 실행하는 과정임.ChatbotData.csv(일상대화 Q&A
쌍).# --- 0. 라이브러리 로드 ---
install.packages("keras3") # Keras 3 R 패키지(최초 1회)
install.packages("tensorflow") # 백엔드(최초 1회)
library(reticulate) # 파이썬 가상환경 연결
conda_create("my_env_310", python_version="3.10") # 파이썬 버전이 3.10인 콘다 가상환경 생성(keras3에서 사용되는 tensorflow는 파이썬 버전이 3.10일 때 설치가 가장 용이함)
use_condaenv("my_env_310") # 파이썬 버전이 3.10인 콘다 가상환경 설정
keras3::install_keras(backend = "tensorflow") # Keras 3 및 TF 설치(최초 1회만 하면 됨)
# 만약 keras3::install_keras(backend = "tensorflow")로 설치오류가 나면 다음 코드를 실행해볼 것.
library(reticulate)
library(keras3)
conda_install(
envname = "my_env_310",
packages = c("tensorflow", "keras"), # 설치할 파이썬 패키지명
pip = TRUE # pip를 사용헤야 최신 버전이 설치됨
)
library(keras3) # Keras 3 R 패키지(멀티-백엔드 지원)
library(tensorflow) # Keras의 백엔드 '엔진'으로 TensorFlow를 사용
library(tidyverse) # 텍스트 처리를 위한 str_split, str_length 등 함수 활용.
library(readr) # read_csv로 CSV 파일 읽기
songys/Chatbot_data 깃허브 저장소의
ChatbotData.csv 파일을 사용함.# --- 1. 데이터 로드 및 전처리 ---
# 챗봇 데이터셋 URL
data_url <- "https://raw.githubusercontent.com/songys/Chatbot_data/master/ChatbotData.csv"
# 로컬에 저장할 파일명
csv_file <- "ChatbotData.csv"
download.file(data_url, csv_file)
# readr 패키지의 read_csv 함수로 CSV 파일을 읽어 R의 tibble로 로드
data <- read_csv(csv_file, show_col_types = FALSE) # 각 칼럼의 유형이 무엇인지를 보여주지 않아도 된다고 설정.
data <- data <- data[1:2000, ]
# -----------------------------------------------------------------
# ※중요※: 전체 데이터(약 12,000개)를 모두 사용하면 훈련에 매우 오래 걸림.
# GPU가 없는 환경([EX] 개인 노트북)에서는 빠른 실습 및 테스트를 위해
# 데이터 양을 2000개 정도로 줄일 것을 권고.
# 구글 Colab은 잦은 오류와 패키지 설치 소요 시간을 고려하여 사용하지 않을 계획임.
# -----------------------------------------------------------------
t(tab): 문장의 시작(start of
sequence)을 알리는 토큰으로 사용.n(newline): 문장의 종료(end of
sequence)를 알리는 토큰으로 사용.A_input(디코더 입력): “t” + “원본 답변”([EX] “t그냥
있어”)A_target(디코더 타겟): “원본 답변” + “n”([EX] “그냥
있어n”)# 1. 디코더 입력(decoder input): "답변" 앞에 't'를 붙임.
# 이유: 디코더가 '시작' 신호를 보고 첫 글자 "그"를 예측하도록 훈련시키기 위함.
data$A_input <- str_c("t", data$A)
# 2. 디코더 타겟(Decoder Target): "답변" 뒤에 'n'을 붙임.
# 이유: 디코더가 마지막 글자 "어"를 보고 '종료' 신호를 예측하도록 훈련시키기 위함.
data$A_target <- str_c(data$A, "n")
# 데이터 가공 결과 확인을 위해 상위 3개 행의 특정 열을 출력
head(data[, c("Q", "A_input", "A_target")], 3)
t, n 때문에) 사전을 분리하여
구축함.# --- 2. 토큰화 및 사전 구축 ---
# '글자(음절)' 단위로 챗봇을 만듦.
# 1. 입력(질문) 사전에 사용될 모든 고유 글자 추출
input_chars <- data$Q %>% # 'data'의 'Q'(질문) 열을 선택
str_split("") %>% # 모든 문장을 글자 단위로 쪼개 리스트로 만듦
unlist() %>% # 리스트를 하나의 긴 벡터로 펼침
unique() %>% # 중복된 글자를 모두 제거
sort() # 가나다 순으로 정렬
# 2. 출력(답변) 사전에 사용될 모든 고유 글자 추출
# (시작/종료 토큰(t, n)이 반드시 포함되어야 함)
target_chars <- c(data$A_input, data$A_target) %>% # 디코더 입력과 타겟 데이터를 하나로 합침
str_split("") %>% # 모든 문장을 글자 단위로 쪼갬
unlist() %>% # 하나의 긴 벡터로 펼침
unique() %>% # 중복 글자 제거(이때 t, n 포함)
sort() # 가나다 순으로 정렬
# 3. 사전의 크기(원핫 인코딩의 차원 수가 됨)
num_encoder_tokens <- length(input_chars) # 인코더(질문) 사전의 고유 글자 수(모든 질문에 등장하는 고유 글자의 총 개수)
num_decoder_tokens <- length(target_chars) # 디코더(답변) 사전의 고유 글자 수(모든 답변에 등장하는 고유 글자의 총 개수)
# 사전 크기 출력
cat("입력(질문) 고유 글자 수:", num_encoder_tokens, "n")
cat("출력(답변) 고유 글자 수:", num_decoder_tokens, "n")
dict 대신 ’이름을 지닌 벡터(named
vector)’를 사용하여 매핑을 구현함.# 4. 글자 -> 인덱스 매핑 생성(R에서는 이름이 있는 리스트/벡터 사용)
#([EX] ' ' -> 1, '!' -> 2, '?' -> 3 ...)
input_token_index <- 1:num_encoder_tokens # 1부터 사전 크기까지의 숫자 시퀀스 생성
names(input_token_index) <- input_chars # 숫자 시퀀스의 '이름'으로 글자 벡터를 할당
#([EX] 't' -> 1, 'n' -> 2, ' ' -> 3 ...)
target_token_index <- 1:num_decoder_tokens # 1부터 사전 크기까지의 숫자 시퀀스 생성
names(target_token_index) <- target_chars # 숫자 시퀀스의 '이름'으로 글자 벡터를 할당
# 5. 인덱스 -> 글자 매핑(예측 결과 해독 시 사용)
#([EX] 1 -> 't', 2 -> 'n', 3 -> ' ' ...)
reverse_target_char_index <- target_chars # 글자 벡터 생성
names(reverse_target_char_index) <- 1:num_decoder_tokens # 글자 벡터의 '이름'으로 인덱스를 할당
# 6. 모든 문장을 동일한 길이로 맞추기 위해 가장 긴 문장 길이 찾기.
max_encoder_seq_length <- max(str_length(data$Q)) # 질문 중 가장 긴 글자 수
max_decoder_seq_length <- max(str_length(data$A_target)) # 답변 중 가장 긴 글자 수(t, n 포함)
# 최대 길이 출력
max_encoder_seq_length # 최대 질문 길이
max_decoder_seq_length # 최대 답변 길이
[samples, timesteps, features]
samples: 문장(Q&A 쌍)의 개수(챗봇 csv 파일의 행 수.
\(\Rightarrow\) [EX] 2000쌍.timesteps: 문장의 최대 길이(패딩된 길이). \(\Rightarrow\) [EX] 질문(인코더) 35자,
답변(디코더) 24자.features: 사전의 크기(원핫 벡터의 차원). \(\Rightarrow\) [EX] 질문(인코더)에 사용된 총
글자 수 1105자, 답변(디코더)에 사용된 총 글자 수 1038자.encoder_input_data: 인코더 입력.decoder_input_data: 디코더 입력.decoder_target_data: 디코더 타겟.# --- 3. 데이터 벡터화(원핫 인코딩) ---
# 총 샘플 수(문장 쌍의 개수)
num_samples <- nrow(data)
# 1. 인코더 입력(질문): [2000, 최대 질문 길이, 질문사전 크기]
# R의 array() 함수를 사용해 0으로 채워진 3차원 배열을 미리 생성해둠.
encoder_input_data <- array(0L, dim = c(num_samples, max_encoder_seq_length, num_encoder_tokens))
# 2. 디코더 입력(답변 - 시작 토큰 t 포함): [2000, 최대 답변 길이, 답변사전 크기]
decoder_input_data <- array(0L, dim = c(num_samples, max_decoder_seq_length, num_decoder_tokens))
# 3. 디코더 타겟(답변 - 종료 토큰 n 포함, 한 스텝 밀림): [2000, 최대 답변 길이, 답변사전 크기]
decoder_target_data <- array(0L, dim = c(num_samples, max_decoder_seq_length, num_decoder_tokens))
# 모든 샘플(문장 쌍)에 대해 루프를 돎
for(i in 1:num_samples) {
# 텍스트를 글자 리스트로 쪼갬
input_text <- str_split(data$Q[i], "")[[1]] # i번째 질문을 글자 벡터로 변환
target_input_text <- str_split(data$A_input[i], "")[[1]] # i번째 디코더 입력을 글자 벡터로 변환
target_text <- str_split(data$A_target[i], "")[[1]] # i번째 디코더 타겟을 글자 벡터로 변환
# 1. 인코더 입력 벡터화:(i, t, char_idx) 위치에 1을 할당
for(t in 1:length(input_text)) { # 현재 문장의 글자 수만큼 반복
char <- input_text[t] # t번째 글자(사전에서 char의 인덱스)
# [i번째 문장, t번째 글자,] 위치에 1을 찍음
encoder_input_data[i, t, input_token_index[[char]]] <- 1
} # 인코더 입력 벡터화 루프 종료
# 2. 디코더 입력 벡터화
for(t in 1:length(target_input_text)) { # 현재 문장의 글자 수만큼 반복
char <- target_input_text[t] # t번째 글자(사전에서 char의 인덱스)
decoder_input_data[i, t, target_token_index[[char]]] <- 1 # [i, t, char_idx]에 1 할당(i번째 답변[t로 시작하는 답변], t번째 글자, char_idx번째 인덱스에 1이라는 숫자 부여)
} # 디코더 입력 벡터화 루프 종료
# 3. 디코더 타겟 벡터화(교사강요용)
for(t in 1:length(target_text)) { # 현재 문장의 글자 수만큼 반복
char <- target_text[t] # t번째 글자
decoder_target_data[i, t, target_token_index[[char]]] <- 1 # [i, t, char_idx]에 1 할당(i번째 답변[n으로 끝나는 답변], t번째 글자, char_idx번째 인덱스에 1이라는 숫자 부여)
} # 디코더 타겟 벡터화 루프 종료
} # 모든 샘플에 대한 루프 종료
# 생성된 3D 배열의 '형태(shape)' 확인
dim(encoder_input_data)
dim(decoder_input_data)
dim(decoder_target_data)
keras_model_sequential()로는 입력 2개, 출력 1개의
복잡한 구조를 만들 수 없음. Keras 함수형 API(Functional
API)를 사용해야 함.# --- 4. Seq2Seq 모델 구축(R keras3) ---
# 하이퍼파라미터: LSTM의 '기억용량'(은닉상태와 셀 상태 벡터의 차원 수)
# 이 숫자가 클수록 더 복잡한 문맥을 기억할 수 있으나, 계산량이 많아짐.
latent_dim <- 256
state_h[은닉상태],
state_c[셀 상태])를 출력하는 부분임.# --- 인코더 정의: 질문을 읽고 문맥 벡터(상태)를 생성 ---
# 1. 인코더의 "입력층"을 정의합니다. 모델의 "입구 1"
# shape = c(NA, num_encoder_tokens):
# NA: 타임스텝(문장 길이)은 가변적일 수 있음을 의미(padding).
# num_encoder_tokens: 피처(특성)는 '질문 사전 크기'(원핫 벡터의 차원)
encoder_inputs <- layer_input(shape = c(NA, num_encoder_tokens), name = "encoder_input")
# 2. 인코더의 "LSTM 레이어"를 '정의'함(아직 연결은 안 함)
# (layer_lstm 논항 설명 참고)
encoder_lstm_layer <- layer_lstm(
units = latent_dim, # 기억 용량(256차원 벡터)
return_state = TRUE, # (핵심!) 마지막 [output, state_h, state_c]를 리스트로 반환
name = "encoder_lstm"
# return_sequences = FALSE(기본값): 인코더는 '중간 출력'이 필요 없고 '마지막 상태'만 필요함.
)
# 3. 입력층과 LSTM 레이어를 '연결'함(함수처럼 호출).
# encoder_inputs가 encoder_lstm_layer를 통과함.
encoder_outputs_list <- encoder_lstm_layer(encoder_inputs)
# 4. 인코더의 최종 출력(문맥 벡터)을 저장합니다.
# return_state = TRUE이므로, 반환값은 3개 요소를 가진 리스트임:
# 1) encoder_outputs_list[[1]]: 마지막 타임스텝의 '출력'(여기선 사용 안 함). "오늘 뭐해?"에서 "?"에 해당. 필요할 리가 없음!
# 2) encoder_outputs_list[[2]]: 마지막 타임스텝의 '은닉상태'(state_h).
# 3) encoder_outputs_list[[3]]: 마지막 타임스텝의 '셀 상태'(state_c).
# 우리는 '문맥'을 전달할 상태 두 가지만 필요함.
encoder_states <- encoder_outputs_list[2:3] # [state_h, state_c](이것이 '문맥 벡터'임)
# --- 디코더 정의: 문맥 벡터를 받아 답변을 생성 ---
# 1. 디코더의 "입력층"을 정의함(모델의 "입구 2").
# (답변 사전 크기인 num_decoder_tokens를 사용)
decoder_inputs <- layer_input(shape = c(NA, num_decoder_tokens), name = "decoder_input")
# 2. 디코더의 "LSTM 레이어"를 '정의'함(가중치는 인코더와 공유하지 않음).
# (layer_lstm 논항 설명 참고)
decoder_lstm_layer <- layer_lstm(
units = latent_dim, # 인코더와 동일한 기억 용량(차원 수가 맞아야 함)
return_sequences = TRUE, # (핵심!) 모든 타임스텝의 출력을 반환(답변 문장 전체)
return_state = TRUE, # 예측(inference) 시 현재 상태를 다음 스텝에 넘겨주기 위해 필요
name = "decoder_lstm"
)
# 3. 디코더 입력과 "인코더의 상태"를 연결함.
# ※※※ Seq2Seq의 가장 중요한 부분 ※※※
# 디코더 LSTM을 호출할 때, initial_state 인자에 인코더가 반환한 '문맥 벡터(encoder_states)'를 주입함!
# 이것이 인코더가 "질문"을 디코더에게 "전달"하는 방식.
decoder_outputs_list <- decoder_lstm_layer(
decoder_inputs,
initial_state = encoder_states # 인코더의 기억을 디코더의 초기 기억으로 설정!
)
# 디코더 LSTM의 실제 출력(예측 시퀀스)은 리스트의 첫 번째 요소임
decoder_outputs <- decoder_outputs_list[[1]]
# 4. 디코더의 "출력층(dense layer)"을 정의함.
# LSTM의 출력(latent_dim)을 '답변사전 크기'로 변환하고,
# 'softmax'를 사용해 각 글자의 확률을 계산함.
decoder_dense_layer <- layer_dense(
units = num_decoder_tokens, # 답변사전 크기(모든 질문에 등장하는 고유 글자의 총 개수 -> 디코더 원핫 벡터의 차원 수)!
activation = "softmax", # 각 글자의 확률 계산!
name = "decoder_output"
)
# 5. 디코더 LSTM 출력과 출력층을 연결함.
decoder_outputs <- decoder_dense_layer(decoder_outputs)
keras_model() 함수로 2개의 입력과 1개의 출력을 가진
훈련용 모델을 완성함.# --- 훈련용 모델(training model) 최종 조립 ---
#
# 훈련용 모델은 [인코더 입력(질문), 디코더 입력(답변시작)]을 받아서 [디코더 타겟(답변)]을 예측하도록 정의함.
# (keras_model 논항 설명 참고)
training_model <- keras_model(
inputs = c(encoder_inputs, decoder_inputs), # 입력이 2개("입구 1", "입구 2")
outputs = decoder_outputs # 출력이 1개
)
# 모델 구조 요약
summary(training_model)
# --- 5. 모델 컴파일 및 학습 ---
# 1. 모델 컴파일
training_model %>% compile(
optimizer = "rmsprop", # RNN/LSTM에 준수한 성능을 보이는 옵티마이저
loss = "categorical_crossentropy", # 다중 클래스(글자) 분류 문제이므로(가장 적절한 글자를 선택하는 거니까)
metrics = "accuracy" # 글자 단위 정확도 모니터링
)
# 2. 모델 학습(fit)
history <- training_model %>% fit(
# 입력(x): Keras 모델이 두 개의 입력을 받으므로 R 리스트로 전달
# 순서가 중요함: inputs = c(encoder_inputs, decoder_inputs) 순서와 일치해야 함
x = list(encoder_input_data, decoder_input_data),
# 출력(y): 디코더 타겟 데이터
y = decoder_target_data,
batch_size = 64, # 한 번에 64개의 Q&A 쌍을 학습
epochs = 20, # 실습을 위해 20으로 설정(성능을 위해선 100~200 이상 권장)
validation_split = 0.2 # 훈련 데이터의 20%를 검증용으로 사용
)
plot(history) # 훈련 과정 시각화 가능
encoder_inputs와
encoder_states를 그대로 재사용.# --- 6. 예측(Inference)을 위한 모델 재구성 ---
# 1. 예측용 인코더 모델
# - 역할: [질문] -> [문맥 벡터(states)]
# - 훈련 시 정의한 encoder_inputs와 encoder_states를 그대로 재사용
encoder_model <- keras_model(encoder_inputs, encoder_states)
summary(encoder_model)
# 2. 예측용 디코더 모델
# 예측 시 디코더는 이전 스텝의 상태(state_h, state_c)를 입력으로 받아야 함.
# 이를 위한 새로운 입력층을 정의함(상태 주입용 '손잡이').
decoder_state_input_h <- layer_input(shape = c(latent_dim), name = "inf_decoder_state_h")
decoder_state_input_c <- layer_input(shape = c(latent_dim), name = "inf_decoder_state_c")
decoder_states_inputs <- c(decoder_state_input_h, decoder_state_input_c)
# 주의: "훈련 시" 정의했던 'decoder_lstm_layer' 객체를 그대로 재사용함.
# 이 레이어에는 이미 훈련된 가중치가 들어 있음.
# 이번엔 initial_state로 encoder_states가 아닌, decoder_states_inputs(이전 스텝의 상태)를 받도록 연결함.
decoder_outputs_list_inf <- decoder_lstm_layer(
decoder_inputs, # 이 입력은 이제 글자 1개짜리 시퀀스가 됨
initial_state = decoder_states_inputs
)
# 예측용 디코더의 출력도 2개임:
decoder_outputs_inf <- decoder_outputs_list_inf[[1]] # (1) 현재 스텝의 출력(글자 확률)
decoder_states_inf <- decoder_outputs_list_inf[2:3] # (2) 다음 스텝에 넘길 '새 상태'[new_h, new_c]
# 주의: "훈련 시" 정의했던 'decoder_dense_layer' 객체도 그대로 재사용함.
decoder_outputs_inf <- decoder_dense_layer(decoder_outputs_inf)
# 최종 예측용 디코더 모델 조립
decoder_model <- keras_model(
# 입력: 2종류(이전 글자 1개, 이전 상태[은닉상태, 셀 상태] 2개)
inputs = c(decoder_inputs, decoder_states_inputs),
# 출력: 2종류(다음 글자 예측 1개, 다음 상태[은닉상태, 셀 상태] 2개)
outputs = c(decoder_outputs_inf, decoder_states_inf)
)
summary(decoder_model)
encoder_model과
decoder_model을 사용하여, 질문이 들어왔을 때 답변을 한
글자씩 생성하는 R 함수를 정의함.# --- 7. R에서 챗봇 응답 생성(디코딩) 함수 정의 ---
# input_text: 사용자가 입력한 질문([EX] "오늘 뭐해?")
decode_sequence <- function(input_text) {
# 1. 입력 문장(질문)을 벡터화(원핫 벡터)
input_seq <- str_split(input_text, "")[[1]] # 질문을 글자 벡터로 쪼갬
# [1, 최대 질문 길이, 질문사전 크기] 형태의 0 배열 준비
input_data <- array(0L, dim = c(1, max_encoder_seq_length, num_encoder_tokens)) # 왜 샘플 수 = 1? 질문은 한 개니까.
# 질문 텍스트를 원핫 벡터로 변환
for(t in 1:length(input_seq)) { # 글자 수만큼 반복
char <- input_seq[t] # t번째 글자 추출
# 사전에 없는 글자는 무시(0으로 남음)
if(char %in% names(input_token_index)) {
# [1번 샘플, t번째 타임스텝, char의 인덱스] 위치에 1을 할당(원핫 인코딩)
input_data[1, t, input_token_index[[char]]] <- 1
} # if문 종료
} # for 루프 종료
# 2. 인코더 실행: encoder_model에 벡터화된 질문을 넣어 '문맥 벡터'를 받음
# states_value는 [state_h, state_c]를 담은 R 리스트가 됨
states_value <- encoder_model %>%
predict(input_data, verbose = 0)
# 3. 디코더 시작: '시작 토큰(t)'을 디코더의 첫 입력으로 준비
# 모양: [1(샘플), 1(타임스텝), 답변사전 크기]
target_seq_data <- array(0L, dim = c(1, 1, num_decoder_tokens))
# 시작 토큰('t')의 인덱스 위치에 1을 할당
target_seq_data[1, 1, target_token_index[["t"]]] <- 1
# 루프 중지 플래그
stop_condition <- FALSE
# 생성된 답변을 저장할 변수
decoded_sentence <- ""
# 4. 글자 생성 루프 시작(자기회귀 방식)
while(!stop_condition) {
# 5. decoder_model 실행(가장 핵심적인 예측 부분)
# 입력: (1) 이전 스텝의 글자(target_seq_data), (2) 이전 스텝의 상태(states_value)
output_list <- decoder_model %>%
predict(list(target_seq_data, states_value), verbose = 0)
# 출력:(1) 현재 글자 예측(output_tokens),(2) 다음 스텝에 넘길 '새 상태'
output_tokens <- output_list[[1]] #(1, 1, vocab_size) 형태의 확률 분포
states_value <- output_list[2:3] # [new_state_h, new_state_c](다음 루프에 사용됨)
# 6. 가장 확률이 높은 글자(인덱스)를 선택
# output_tokens[1, 1, ]는 현재 타임스텝의 글자별 확률 벡터임
sampled_token_index <- which.max(output_tokens[1, 1, ])
# 인덱스를 실제 글자로 변환
sampled_char <- reverse_target_char_index[[as.character(sampled_token_index)]]
# 생성된 글자를 문장에 추가
decoded_sentence <- str_c(decoded_sentence, sampled_char)
# 7. 종료조건 확인
# 예측된 글자가 '종료 토큰(n)'이거나, 최대 문장 길이를 넘으면 루프 종료
if(sampled_char == "n" || str_length(decoded_sentence) > max_decoder_seq_length) {
stop_condition <- TRUE # 루프 중지(종료조건을 충족하지 않았다면 다음 스텝으로 넘어감)
}
# 8. 다음 스텝 준비
# 방금 예측한 글자 인덱스(sampled_token_index)를 다음 스텝의 '디코더 입력'으로 설정
# 디코더 입력 배열 초기화(0으로 구성된 3D 텐서): [1(샘플), 1(타임스텝), 답변사전 크기]
target_seq_data <- array(0L, dim = c(1, 1, num_decoder_tokens))
target_seq_data[1, 1, sampled_token_index] <- 1 # 예측된 글자 인덱스에 1 할당
# 'states_value'는 이미 위에서 업데이트되었으므로 그대로 다음 루프에 사용
} # while 루프 종료
# 'n' 토큰을 제거하고 반환
return(str_trim(str_replace(decoded_sentence, "n", "")))
}
# --- 8. R에서 챗봇 함수 실행 ---
# 테스트할 질문 목록
test_q <- c(
"오늘 뭐해?",
"심심하다",
"날씨 좋다",
"고마워",
"사랑해",
"배고파",
"넌 누구야?"
)
# 각 질문에 대해 챗봇의 답변 생성
for(q in test_q) {
# 학습된 모델로 답변 생성
response <- decode_sequence(q)
# 결과 출력
cat("질문:", q, "n")
cat("답변:", response, "n---n")
} # for 루프 종료